--- 手表UI框架，实现窗口栈管理、动画、光标、UI定时器
-- @module ui
-- @author 小强
-- @copyright openLuat.com
-- @release 2017.10.31

module(..., package.seeall)

local wstack = {} -- 窗口栈
local timers = {} -- ui定时器
local reentries = 0
local wuid = 0
local keyCallbacks = {}
local locked = false

local function draw()
    if reentries == 0 and #wstack ~= 0 then
        -- 绘制新窗口总是设置为第一个图层显示且为黑底白字
        disp.setlayershow(true, false, false)
        disp.setactivelayer(0)
        disp.setlayerstartpos(0, VSCREEN_X, VSCREEN_Y)
        disp.setbkcolor(BLACK)
        disp.setcolor(WHITE)
        update()
    end
end

local function __enter(w, isNew)
    wuid = wuid + 1
    -- 进入新的窗口停止并清空ui定时器
    for _, t in ipairs(timers) do t:stop() end
    timers = {}
    keyCallbacks = {} -- 清空按键处理
    reentries = reentries + 1
    w.enter(isNew)
    reentries = reentries - 1
end

--- 进入新的窗口
-- @param w 新窗口对象ui.window
-- @return 无
-- @usage ui.enter(ui.window{draw=function() end})
function enter(w)
    if w.popup == true or w.notice == true then lcd.turnon() end -- UI通知可能会在熄屏状态下弹出，因此需要打开背光

    if #wstack ~= 0 then
        if wstack[#wstack].popup == true then -- 进入新窗口时如果有popup自动清除掉
            table.remove(wstack, #wstack)
        elseif wstack[#wstack].notice == true and w.notice == true then -- 产生不同的通知窗口时自动清除旧的通知窗口
            table.remove(wstack, #wstack).exit()
        else
            wstack[#wstack].losefocus()
        end
    end
    table.insert(wstack, w)
    __enter(w, true) -- 为新创建的窗口在调用enter回调时增加一个标记
    draw()
end

--- 返回上一层窗口
-- @param 无
-- @return 无
-- @usage ui.goback()
function goback()
    if wstack[#wstack].root == true then return end
    reentries = reentries + 1
    table.remove(wstack, #wstack).exit()
    if #wstack ~= 0 then __enter(wstack[#wstack]) end
    reentries = reentries - 1
    draw()
end

--- 刷新当前窗口
-- @param 无
-- @return 无
-- @usage ui.update()
function update()
    if locked then return end
    wstack[#wstack].draw()
end

local function empty() end

local windowmt = { __index = function() return empty end }

--- 创建窗口
-- @param 无
-- @return 窗口对象
-- @usage ui.window{draw=function() end}
function window(o)
    setmetatable(o, windowmt)
    return o
end

--- 创建光标
-- @param 光标参数: x1,y1 起始坐标 width,height 光标宽高 intreval 闪烁间隔默认500
-- @return 光标对象
-- @usage
-- local cursor = ui.cursor{x1=0,y1=0,width=2,height=10}
-- cursor.blink(true) -- 开始闪烁
-- cursor.blink(false) -- 停止闪烁
-- cursor.draw() -- 绘制光标
function cursor(o)
    local ctimer
    o.state = false
    o.blinking = false
    o.draw = function(dontupdate)
        local dispfn = disp.drawrect
        if not o.blinking then
            ctimer:stop()
        else
            ctimer:start()
            if not o.state then dispfn = disp.clear end
            o.state = not o.state
        end
        dispfn(o.x1, o.y1, o.x1 + o.width - 1, o.y1 + o.height, o.color)
        if not dontupdate then
            disp.update(o.x1, o.y1, o.x1 + o.width - 1, o.y1 + o.height)
        end
    end
    o.blink = function(blinking)
        o.blinking = blinking
        if blinking then
            ctimer:start()
        else
            ctimer:stop()
        end
    end
    ctimer = timer { interval = o.interval or 500, callback = o.draw }
    return o
end

--- 创建动画
-- @param 动画参数
-- name 动画图片文件前缀名称
-- x1,y1,x2,y2 动画显示区域，默认全屏
-- interval 播放间隔 默认500
-- n 动画总帧数
-- layer 图层id 默认为当前图层
-- @return 窗口对象
-- @usage
-- local anim = {name='demoanim', n=3}
-- anim.start() -- 按照500ms的间隔轮流播放demoanim1.png, demoanim2.png, demoanim3.png
function animation(anim)
    local index = 0
    local animtimer = timer { interval = anim.interval or 500 }
    anim.draw = anim.draw or function(i)
        if anim.layer then disp.setactivelayer(anim.layer) end
        disp.clear(anim.x1, anim.y1, anim.x2, anim.y2)
        disp.putimage(anim.name .. i .. '.png', anim.x1, anim.y1)
        disp.update(anim.x1, anim.y1, anim.x2, anim.y2)
    end
    anim.start = function()
        index = 0
        anim.draw(index + 1)
        animtimer:start()
    end
    anim.stop = function()
        animtimer:stop()
    end
    animtimer.callback = function()
        anim.draw(index + 1)
        if index + 1 == anim.n and anim.once then return end
        index = (index + 1) % anim.n
        animtimer:start()
    end
    return anim
end

-- UI定时器
local timermt = {}
timermt.__index = timermt
--- ui.timer() 创建定时器
-- @param 定时器参数, interval 间隔 callback 回调
-- @return 无
-- @usage
-- local demotimer = timer{interval = 1000, callback = function() print('demo ui timer timeout') end}
function timer(t)
    setmetatable(t, timermt)
    return t
end

--- ui.timer():start() 启动UI定时器
-- @param 无
-- @return 无
-- @usage demotimer:start()
function timermt:start()
    self.tid = sys.timer_start(self.callback, self.interval)
    table.insert(timers, self)
end

--- ui.timer():stop() 停止UI定时器
-- @param 无
-- @return 无
-- @usage demotimer:start()
function timermt:stop()
    if not self.tid then return end
    sys.timer_stop(self.tid)
    self.tid = nil
end

lcd.onLcdOff(function()
    -- 将锁屏界面插入窗口栈 通知、锁屏、开关机欢迎界面在锁屏时不插入锁屏时钟
    if wstack[#wstack].boot ~= true and wstack[#wstack].lockscreen ~= true and wstack[#wstack].notice ~= true then
        enter(clock())
    end
    -- 清除所有UI定时器
    for _, t in ipairs(timers) do t:stop() end
    timers = {}
end)

function lock()
    locked = true
    lcd.disable(true)
end

function unlock()
    locked = false
    lcd.disable(false)
end

--- 键盘消息处理
-- 键值，按键状态定义
KEY_POWER = 0xffff
KEY_CALL = 0x0101
KEY_HOME = 0

KEY_DOWN = 0
KEY_UP = 1
KEY_LONGPRESS = 2

local keyWindow
local function onLongpress(key)
    if keyCallbacks[key * 4 + KEY_LONGPRESS] then
        keyCallbacks[key * 4 + KEY_LONGPRESS]()
    else
        if key == KEY_POWER then
            if not sim.getstatus() then
                ui.enter(welcome('poweroff'))
            else
                sys.publish('SOS_REQ') -- 插卡状态下按开关机键SOS功能
            end
        end
    end
end

function onkey(key, state, callback)
    keyCallbacks[key * 4 + state] = callback
end

local function handleKey(key, state)
    if pm.isleep('lcd') then -- 灭屏状态下松开按键亮屏
        if state == KEY_UP then
            pm.wake('key') -- 先唤醒系统,不然如果当前界面有图片硬解会重启
            rtos.sleep(5)
            ui.update()
            lcd.turnon()
            pm.sleep('key') -- 接下来的睡眠处理由lcd模块负责
        end
    else
        local id = key * 4 + state
        if keyCallbacks[id] then
            keyCallbacks[id]()
        elseif (key == KEY_POWER and state == KEY_DOWN) or (key == KEY_HOME and state == KEY_UP) then
            goback()
        end
    end
end

local pressedKey
rtos.init_module(rtos.MOD_KEYPAD, 0, 0x07, 0x07)
rtos.on(rtos.MSG_KEYPAD, function(msg)
    if locked then return end
    local key = msg.key_matrix_row * 256 + msg.key_matrix_col
    log.debug('ui.KEYPAD', msg.key_matrix_row, msg.key_matrix_col, msg.pressed)

    if msg.pressed then
        if pressedKey == KEY_CALL and key == KEY_POWER then
            if nvm.get('ft_result') ~= 'pass' then
                ui.enter(functiontest())
            end
        end
        pressedKey = key
        keyWindow = wuid
        sys.timer_start(onLongpress, 2000, key)
        if not pm.isleep('lcd') then lcd.turnon('key') end
    else
        pressedKey = nil
        sys.timer_stop(onLongpress, key)
        lcd.turnoff('key')
        if keyWindow ~= wuid then -- 不在一个界面下的完整按键流程不处理
            return
        end
    end

    handleKey(key, msg.pressed and KEY_DOWN or KEY_UP)
end)

local function checkCoord(v)
    if v < 0 then return 0
    elseif v > 127 and v ~= 140 then return 127
    else return v
    end
end

-- 注册触屏消息
local touchEvent = { 'pendown', 'penmove', 'penup' }
local touchWindow
rtos.init_module(rtos.MOD_TP)
rtos.on(rtos.MSG_TP, function(msg)
    if pm.isleep('lcd') or locked then return end -- 灭屏状态下不处理任何触屏消息

    local event = touchEvent[msg.pen_state + 1]
    log.debug('ui.KEYPAD', event, msg.x, msg.y)
    local x, y = checkCoord(msg.x), checkCoord(msg.y)

    if event == 'pendown' then
        touchWindow = wuid
        lcd.turnon('touch')
    else
        lcd.turnoff('touch')
        if touchWindow ~= wuid then -- 如果是之前窗口发生的触摸消息，则忽略掉
            return
        end
    end

    if x == 100 and y == 140 then
        handleKey(KEY_HOME, event == 'pendown' and KEY_DOWN or KEY_UP)
        return
    end
    wstack[#wstack][event](x, y)
end)
